CDKへの段階移行に使えるかも?CDKからSAMテンプレートを読み込んでリソースを追加作成してみた
CX事業本部の岩田です。
CDKすごく便利そうですよね??便利そうなので、この際インフラの管理は全てCDKに移行して...と言いたいところですが、既にCloudFormationやSAMで諸々の環境が構築済みで移行コストやリスクを懸念してCDKの利用に踏み切れない といったケースは多いと思います。そういったケースに対応するため、比較的移行コストとリスクを下げつつSAMとCDKを共存させる方法について調べてみました。
環境等
- OS : macOS Mojave 10.14.6
- Node.js : v10.15.1
- AWS CDK : 1.4.0 (build 175471f)
やること
元々SAMでリソースを管理しているサーバーレスアプリケーションがあります。 現在SAMで管理しているリソースの中で記述が冗長になりがちな部分をCDKに切り出します。 現状デプロイはにSAMを利用しているので、CDK導入後もデプロイは継続してSAMを利用することとします。 同様にSAM CLIを使ってローカルテストが実施できることも必須要件です。
現行の構成
こんなディレクトリ構成です。docs/swagger.yml
でAPIの仕様を定義しており、SAMテンプレートからも参照しています。
├── docs │ └── swagger.yml ├── sam.yml └── src ├── create_pet.py ├── get_pet.py └── list_pets.py
現行のSAMテンプレート
現行のSAMテンプレートです。
AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: 'CDK With SAM' Globals: Function: Runtime: python3.7 Resources: ListPets: Type: 'AWS::Serverless::Function' Properties: CodeUri: src Handler: list_pets.handler Role: !GetAtt LambdaExecuteRole.Arn GetPet: Type: 'AWS::Serverless::Function' Properties: CodeUri: src Handler: get_pet.handler Role: !GetAtt LambdaExecuteRole.Arn CreatePet: Type: 'AWS::Serverless::Function' Properties: CodeUri: src Handler: create_pet.handler Role: !GetAtt LambdaExecuteRole.Arn PetStoreApi: Type: AWS::Serverless::Api Properties: StageName: prd DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: docs/swagger.yml LambdaExecuteRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - 'lambda.amazonaws.com' Action: sts:AssumeRole Policies: - PolicyName: 'lambda-permissions' PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: - "*" LambdaPermissionListPets: Type: "AWS::Lambda::Permission" Properties: Action: lambda:InvokeFunction FunctionName: !Ref ListPets Principal: apigateway.amazonaws.com LambdaPermissionGetPet: Type: "AWS::Lambda::Permission" Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetPet Principal: apigateway.amazonaws.com LambdaPermissionCreatePet: Type: "AWS::Lambda::Permission" Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreatePet Principal: apigateway.amazonaws.com ListPetsLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${ListPets} RetentionInDays: 731 GetPetLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${GetPet} RetentionInDays: 731 CreatePetLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${CreatePet} RetentionInDays: 731
Lambdaを定義する部分はまあ良いのですが、Cloudwatch Logsのロググループの定義とAPI GwからLambdaを呼び出すためのパーミッションの定義をLambdaの数だけ繰り返しており(ハイライト箇所)非常に冗長です。こういったループ処理はプログラムにやらせたいですね。 とりあえず検証用にデプロイしておきます。
sam package --template-file sam.yml --s3-bucket <適当なS3バケット> --output-template-file output.yml sam deploy --template-file output.yml --stack-name sam-with-cdk --capabilities CAPABILITY_IAM
CDKと共存させてみる
早速CDKと共存させてみましょう。cdk
というディレクトリを作って、ここにCDK関連の諸々を突っ込んでいきます。
mkdir cdk cd cdk cdk init app --language=typescript
ディレクトリ構成はこんな感じに変わります
├── cdk │ ├── README.md │ ├── bin │ │ └── cdk.ts │ ├── cdk.json │ ├── cdk.out │ │ ├── CdkStack.template.json │ │ ├── cdk.out │ │ └── manifest.json │ ├── lib │ │ └── cdk-stack.ts │ ├── node_modules │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── docs │ └── swagger.yml ├── output.yml ├── sam.yml ├── output.yml # sam packageでパッケージ後のテンプレート └── src ├── create_pet.py ├── get_pet.py └── list_pets.py
CDKのコードを書く!!
準備ができたのでCDKのコードを書いていきます。 今回は既存のSAMテンプレートから
AWS::Lambda::Permission
のリソースAWS::Logs::LogGroup
のリソース
の定義を削除し、CDKで生成することを目標とします。
まずはCDKから既存のSAMテンプレートを読み込みます。CDKにはCfnInclude
というクラスが用意されているので、このクラスのコンストラクタにSAMテンプレートを渡してインスタンスを生成します。なお、ここで指定するテンプレートはsam package
でパッケージ後のテンプレートです。
const sam_output_path = path.resolve(__dirname, '../../output.yml'); const cfn_template = <any> new cdk.CfnInclude(this, "PackagedSamTemplate", { template: yaml.parse(fs.readFileSync(sam_output_path).toString()) });
続いて読み込んだテンプレートからAWS::Serverless::Function
のリソースを抽出します
const resources = cfn_template.template.Resources; const funcs = Object.keys(resources).filter(key => { return resources[key].Type === 'AWS::Serverless::Function'; });
これでfuncs
の中にリソースのキー一覧が入ります。
今回のテンプレートだと、funcs
の中身は['ListPets', 'GetPet', 'CreatePet']
になります。
リソースのキーが全て取得できたので、ループしながらロググループの作成とパーミッションの設定を行います。
logGroupName
やfunctionName
はSAMテンプレートで定義されたLambdaのFunctionNameを動的に利用するため、Fn
クラスのスタティックメソッドsub
を利用します。このメソッドがCloudFormationのFn::Sub
相当の動きになります。
funcs.forEach(func_key => { new LogGroup(this, `${func_key}LogGroup`, { retention: RetentionDays.TWO_YEARS, logGroupName: Fn.sub(`/aws/lambda/\${${func_key}}`) }); new CfnPermission(this, `LamberPermission${func_key}`, { principal: 'apigateway.amazonaws.com', action: 'lambda:InvokeFunction', functionName: Fn.sub(`\${${func_key}}`) }); });
最終形
最終的なCDKのコードです。
import * as cdk from '@aws-cdk/core'; import {LogGroup, RetentionDays} from '@aws-cdk/aws-logs'; import * as fs from 'fs'; import * as yaml from 'yaml'; import * as path from 'path'; import { Fn } from '@aws-cdk/core'; import { CfnPermission} from '@aws-cdk/aws-lambda'; export class CdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here // Package後のSAMテンプレートを読み込み const sam_output_path = path.resolve(__dirname, '../../output.yml'); const cfn_template = <any> new cdk.CfnInclude(this, "PackagedSamTemplate", { template: yaml.parse(fs.readFileSync(sam_output_path).toString()) }); // SAMテンプレートからTypeがAWS::Serverless::Functionのリソースを抽出 const resources = cfn_template.template.Resources; const funcs = Object.keys(resources).filter(key => { return resources[key].Type === 'AWS::Serverless::Function'; }); funcs.forEach(func_key => { // LogGroupを作成する new LogGroup(this, `${func_key}LogGroup`, { retention: RetentionDays.TWO_YEARS, logGroupName: Fn.sub(`/aws/lambda/\${${func_key}}`) }); // ApiGWからLambdaをInvokeするためのパーミッションを設定する new CfnPermission(this, `LamberPermission${func_key}`, { principal: 'apigateway.amazonaws.com', action: 'lambda:InvokeFunction', functionName: Fn.sub(`\${${func_key}}`) }); }); } }
CDKに移植できたのでSAMテンプレートからは
AWS::Lambda::Permission
のリソースAWS::Logs::LogGroup
のリソース
の記述を削除します。
AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: 'CDK With SAM' Globals: Function: Runtime: python3.7 Resources: ListPets: Type: 'AWS::Serverless::Function' Properties: CodeUri: src Handler: list_pets.handler Role: !GetAtt LambdaExecuteRole.Arn GetPet: Type: 'AWS::Serverless::Function' Properties: CodeUri: src Handler: get_pet.handler Role: !GetAtt LambdaExecuteRole.Arn CreatePet: Type: 'AWS::Serverless::Function' Properties: CodeUri: src Handler: create_pet.handler Role: !GetAtt LambdaExecuteRole.Arn PetStoreApi: Type: AWS::Serverless::Api Properties: StageName: prd DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: docs/swagger.yml LambdaExecuteRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - 'lambda.amazonaws.com' Action: sts:AssumeRole Policies: - PolicyName: 'lambda-permissions' PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: - "*"
SAM & CDKでデプロイしてみる!
準備ができたのでSAMとCDKを組み合わせてデプロイしてみます。 一点注意したいのですが、一気にSAM & CDKでのデプロイに移行すると、存在するロググループを作成しにいってエラーになってしまいます。SAM & CDKの構成に移行するため、一旦現在のSAMテンプレートでデプロイを行いロググループを削除します。 もし稼働中のシステムをSAM & CDKの構成に移行する場合は要注意です!!
sam package --template-file sam.yml --s3-bucket <適当なS3バケット> --output-template-file output.yml sam deploy --template-file output.yml --stack-name sam-with-cdk --capabilities CAPABILITY_IAM
これで準備ができたので改めてSAM & CDKでデプロイを行います。cdk synth
というコマンドを実行することでCDKからCloudFormationのテンプレートが出力できるので
- sam package
- cdk synth
- sam deploy
と順番に実行し、デプロイを行います。
sam package --template-file sam.yml --s3-bucket <適当なS3バケット> --output-template-file output.yml cd cdk cdk synth > cdk_output.yml sam deploy --template-file cdk_output.yml --stack-name sam-with-cdk --capabilities CAPABILITY_IAM
マネコンから確認してみましょう
CDKで追加したリソースもバッチリ作成できていますね!
ちなみにcdk synth
の出力はこんな感じでした
Description: CDK With SAM Transform: AWS::Serverless-2016-10-31 AWSTemplateFormatVersion: "2010-09-09" Globals: Function: Runtime: python3.7 Resources: ListPets: Type: AWS::Serverless::Function Properties: CodeUri: s3://some-s3bucket/21a80cbe04a4a456dc36ec1643dfcb20 Handler: list_pets.handler Role: Fn::GetAtt: - LambdaExecuteRole - Arn GetPet: Type: AWS::Serverless::Function Properties: CodeUri: s3://some-s3bucket/21a80cbe04a4a456dc36ec1643dfcb20 Handler: get_pet.handler Role: Fn::GetAtt: - LambdaExecuteRole - Arn CreatePet: Type: AWS::Serverless::Function Properties: CodeUri: s3://some-s3bucket/21a80cbe04a4a456dc36ec1643dfcb20 Handler: create_pet.handler Role: Fn::GetAtt: - LambdaExecuteRole - Arn PetStoreApi: Type: AWS::Serverless::Api Properties: StageName: prd DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: s3://some-s3bucket/fa270f10518978fcd487a80e0c9c0f0d LambdaExecuteRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: lambda-permissions PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - "*" ListPetsLogGroup297C159C: Type: AWS::Logs::LogGroup Properties: LogGroupName: Fn::Sub: /aws/lambda/${ListPets} RetentionInDays: 731 UpdateReplacePolicy: Retain DeletionPolicy: Retain Metadata: aws:cdk:path: CdkStack/ListPetsLogGroup/Resource LamberPermissionListPets: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::Sub: ${ListPets} Principal: apigateway.amazonaws.com Metadata: aws:cdk:path: CdkStack/LamberPermissionListPets GetPetLogGroupD6EA9D77: Type: AWS::Logs::LogGroup Properties: LogGroupName: Fn::Sub: /aws/lambda/${GetPet} RetentionInDays: 731 UpdateReplacePolicy: Retain DeletionPolicy: Retain Metadata: aws:cdk:path: CdkStack/GetPetLogGroup/Resource LamberPermissionGetPet: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::Sub: ${GetPet} Principal: apigateway.amazonaws.com Metadata: aws:cdk:path: CdkStack/LamberPermissionGetPet CreatePetLogGroup292AC3D6: Type: AWS::Logs::LogGroup Properties: LogGroupName: Fn::Sub: /aws/lambda/${CreatePet} RetentionInDays: 731 UpdateReplacePolicy: Retain DeletionPolicy: Retain Metadata: aws:cdk:path: CdkStack/CreatePetLogGroup/Resource LamberPermissionCreatePet: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::Sub: ${CreatePet} Principal: apigateway.amazonaws.com Metadata: aws:cdk:path: CdkStack/LamberPermissionCreatePet CDKMetadata: Type: AWS::CDK::Metadata Properties: Modules: aws-cdk=1.4.0,@aws-cdk/assets=1.4.0,@aws-cdk/aws-cloudwatch=1.4.0,@aws-cdk/aws-ec2=1.4.0,@aws-cdk/aws-events=1.4.0,@aws-cdk/aws-iam=1.4.0,@aws-cdk/aws-kms=1.4.0,@aws-cdk/aws-lambda=1.4.0,@aws-cdk/aws-logs=1.4.0,@aws-cdk/aws-s3=1.4.0,@aws-cdk/aws-s3-assets=1.4.0,@aws-cdk/aws-sqs=1.4.0,@aws-cdk/aws-ssm=1.4.0,@aws-cdk/core=1.4.0,@aws-cdk/cx-api=1.4.0,@aws-cdk/region-info=1.4.0,jsii-runtime=node.js/v10.15.1
読み込んだSAMテンプレートに色々追加されているのが分かると思います
まとめ
今回紹介したような内容であればCloudFormationのマクロを作ってしまうのもありだと思いますが、マクロに比べてCDKの方が環境準備が簡単そうな感触でした。
既存資産のことを考えると、ドラスティックにCDKへ移行するというのは難しいケースもあるかと思いますが、こういったテクニックを組み合わせつつ改善していきたいですね。
CfnInclude
で読み込んだテンプレートをCDK側で更新したい!!というissueも上がっているので、こちらの動向にも注目です。